Skip to content

feat(usbgadget): add CDC-NCM (Ethernet over USB) function#1470

Open
mcuelenaere wants to merge 3 commits into
jetkvm:devfrom
mcuelenaere:claude/optimistic-torvalds-0a8c56
Open

feat(usbgadget): add CDC-NCM (Ethernet over USB) function#1470
mcuelenaere wants to merge 3 commits into
jetkvm:devfrom
mcuelenaere:claude/optimistic-torvalds-0a8c56

Conversation

@mcuelenaere
Copy link
Copy Markdown
Contributor

@mcuelenaere mcuelenaere commented May 17, 2026

Refs #642

Summary

Adds CDC-NCM (Ethernet over USB) as a new USB gadget function — a base layer for future work like eg clipboard sync over IP. Off by default, toggled via the new "Enable Ethernet over USB (CDC-NCM)" checkbox under custom USB devices. Reachable from the host over IPv6 link-local (fe80::) with zero host-side config on Windows, macOS, and Linux.

Why CDC-NCM (not ECM/RNDIS)

Modern cross-platform standard with inbox drivers on Windows 10+, macOS, and Linux. ECM is older and not native on Windows; RNDIS is Microsoft-proprietary and deprecated. A prior attempt (#459) added all four protocols plus NAT and internet sharing but was closed when the usbgadget package was refactored; the kernel-side prerequisite is now merged (rv1106-system#16).

How it works

  • New Devices.Ncm toggle and ncmConfig gadget item registering ncm.usb0.
  • host_addr and dev_addr MACs are derived deterministically from GetDeviceID() (SHA-256, with locally-administered + unicast bits set correctly), so the IPv6 link-local address (kernel-derived from dev_addr via modified EUI-64) is also stable across reboots.
  • After configureUsbGadget commits, a post-transaction hook in usbgadget uses vishvananda/netlink (already a dep) to bring usb0 up and ensures the per-interface IPv6 sysctl isn't disabled. Tear-down on toggle off.
  • A small host-isolation firewall is installed via github.com/google/nftables (pure Go, netlink): an inet jetkvm table with one chain input_usb0 that drops inbound TCP and UDP arriving on usb0. ICMP/ICMPv6 (including NDP and ping) is intentionally untouched so the link stays debuggable. Fail-closed: if the firewall can't be installed, usb0 is rolled back down. nf_tables.ko is loaded lazily via modprobe on first toggle (the rv1106 rootfs ships the module but doesn't auto-load it).
  • Frontend toggle added to UsbDeviceSetting.tsx in the "Custom" preset.
  • A USB endpoint-budget warning: CDC-NCM needs two IN endpoints, and the RV1106 dwc3 controller only has ~7 usable IN endpoints total. Enabling NCM on top of the full default set (now including the audio gadget) overflows the budget and silently breaks NCM's TX. The settings UI now checks IN/OUT endpoint demand live (getUsbEndpointReport RPC) and warns before the user commits to an over-budget combination.

Verification

  • ARM cross-compile clean (SKIP_UI_BUILD=1 make build_dev).
  • Frontend type-check (tsc --noEmit) and lint (oxlint) clean.
  • Manual on-device tests (macOS host):
    • Toggle on → ncm.usb0 present in configfs, usb0 UP with fe80::.../64, ping6 fe80::<mac>%en<N> works immediately.
    • nft list table inet jetkvm shows the input_usb0 chain with the two drop rules; counter on a diagnostic rule confirms packets arrive on usb0.
    • curl -g 'http://[fe80::<mac>%en<N>]/' from the host times out (TCP dropped silently). Same for :443, ssh -6, mDNS over usb0.
    • eth0 web UI, mDNS, and SSH continue to work normally (rules are scoped via iifname == "usb0").
    • Toggle off → nft list table inet jetkvm returns "No such table"; usb0 netdev disappears with the gadget rebind.
  • The endpoint-budget warning is build- and lint-verified; the underlying constraint was confirmed on-device (NCM TX dead at 8 IN, working at 7), but the UI warning itself hasn't had a device pass yet (this PR is still a draft).

Host isolation on usb0

When the toggle is on, an nftables firewall is installed that drops all inbound TCP and UDP on usb0, so the web UI (:80/:443), mDNS, Dropbear/SSH (:22), and any other system service jetkvm_app doesn't directly own are unreachable from the target host. ICMPv4 / ICMPv6 (including NDP and ping) stay allowed so the link remains debuggable and IPv6 neighbor discovery keeps working. This addresses the security concern that a compromised target host would otherwise be able to reach the management plane that's controlling it.

Scope and caveats:

  • Fail-closed: if modprobe nf_tables or the netlink Flush fails, bringUpNcmInterface returns the error and rolls usb0 back down — no scenario where usb0 is exposed without the firewall.
  • The firewall lives in a dedicated inet jetkvm table; nothing else on the device uses nftables today, but the dedicated namespace future-proofs against collisions.
  • Rules are TCP+UDP only. New services that listen on SCTP, DCCP, or raw IP wouldn't be covered, but we don't ship any. ICMP is intentionally allowed.
  • The default-off toggle is still the primary safety net: the firewall isn't even instantiated until the user explicitly enables CDC-NCM. Together they mean the only way a target host reaches JetKVM services is if the operator (a) enables the toggle and (b) defeats the kernel-enforced drop rules.

USB endpoint budget

The RV1106 dwc3 controller exposes IN (device→host) and OUT (host→device) endpoints as separate pools — roughly 7 usable in each — and every composite gadget function claims some. Both directions are tracked, though in practice IN is the binding constraint:

  • HID interrupt-IN (keyboard, wake, each mouse) and audio capture (isochronous IN) cost 1 IN each; keyboard additionally has an interrupt-OUT for LED reports.
  • Mass storage costs 1 IN + 1 OUT (bulk in/out).
  • CDC-ACM and CDC-NCM cost 2 IN + 1 OUT each (bulk-IN + interrupt-IN notification + bulk-OUT).

The full default set + NCM = 8 IN (over the ~7 budget) while OUT stays well under, so IN is what overflows. When the controller runs out, NCM's bulk-IN silently fails to allocate: the interface enumerates and usb0 comes up (RX works), but TX is a black hole. This was a genuinely painful thing to debug while rebasing onto the new audio gadget, so:

  • internal/usbgadget/endpoints.go models per-function IN and OUT cost against independent IN/OUT budgets (constants, with the empirical derivation documented).
  • getUsbEndpointReport RPC returns {exceedsBudget} — a single boolean — for any device selection. The UI only needs to know whether the limit is hit, not the exact count.
  • The USB Devices settings UI rechecks as toggles change and shows an inline warning when the selected set exceeds either budget, suggesting the user free an endpoint (e.g. disable Relative Mouse). It's a warning, not a hard block — hardware budgets can vary and the user may know what they're doing.

Note: this branch is rebased onto current dev, which now includes the USB audio gadget; the budget math accounts for it.

Out of scope (deferred to follow-ups)

  • No IPv4 address assignment (no APIPA, static, or DHCP server).
  • No NAT / internet sharing / bridge mode.
  • No ECM/EEM/RNDIS — NCM only.
  • No nmlite integration; usb0 is independent of eth0.
  • No clipboard sync over this transport (the future work this is unblocking).

Checklist

  • Ran make test_e2e locally and passed
  • Refs Ethernet Passthrough via USB #642
  • One problem per PR (no unrelated changes)
  • Lints pass; CI green
  • Tricky parts are commented in code

🤖 Generated with Claude Code

mcuelenaere added a commit to mcuelenaere/kvm that referenced this pull request May 26, 2026
Adds an `inet jetkvm` table with a single `input_usb0` chain that drops
TCP and UDP arriving on usb0. ICMP/ICMPv6 (including NDP and ping) are
untouched so the link stays debuggable. Installed by applyNcmFirewall
when NCM is toggled on, removed by removeNcmFirewall on toggle off.

Fail-closed: if modprobe or any nftables operation fails, the link is
rolled back and bringUpNcmInterface returns the error -- no scenario
where usb0 is up without the firewall in place. Idempotent: a stale
table from a previous run is deleted before the fresh one is built.

The rv1106 rootfs ships nf_tables.ko in /lib/modules but does not
auto-load it (kernel module auto-load via NETLINK_NETFILTER never
fires here), so applyNcmFirewall does a lazy `modprobe nf_tables` on
first use. Match expressions (meta iifname, meta l4proto, cmp, drop)
are all baked into nf_tables.ko on 5.10, so no additional modprobes
are needed for the production rules.

This closes the security gap that was previously documented as a
known issue in PR jetkvm#1470: with this in place, the web UI, mDNS, SSH
and any other system services are unreachable from the target host
over the CDC-NCM link, while ICMPv6 (NDP + ping) keeps the link
itself debuggable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mcuelenaere mcuelenaere marked this pull request as draft May 26, 2026 19:34
mcuelenaere added a commit to mcuelenaere/kvm that referenced this pull request May 26, 2026
Adds an `inet jetkvm` table with a single `input_usb0` chain that drops
TCP and UDP arriving on usb0. ICMP/ICMPv6 (including NDP and ping) are
untouched so the link stays debuggable. Installed by applyNcmFirewall
when NCM is toggled on, removed by removeNcmFirewall on toggle off.

Fail-closed: if modprobe or any nftables operation fails, the link is
rolled back and bringUpNcmInterface returns the error -- no scenario
where usb0 is up without the firewall in place. Idempotent: a stale
table from a previous run is deleted before the fresh one is built.

The rv1106 rootfs ships nf_tables.ko in /lib/modules but does not
auto-load it (kernel module auto-load via NETLINK_NETFILTER never
fires here), so applyNcmFirewall does a lazy `modprobe nf_tables` on
first use. Match expressions (meta iifname, meta l4proto, cmp, drop)
are all baked into nf_tables.ko on 5.10, so no additional modprobes
are needed for the production rules.

This closes the security gap that was previously documented as a
known issue in PR jetkvm#1470: with this in place, the web UI, mDNS, SSH
and any other system services are unreachable from the target host
over the CDC-NCM link, while ICMPv6 (NDP + ping) keeps the link
itself debuggable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mcuelenaere mcuelenaere force-pushed the claude/optimistic-torvalds-0a8c56 branch 2 times, most recently from 4a6db8c to 532f30c Compare May 26, 2026 21:06
mcuelenaere and others added 3 commits May 29, 2026 14:58
Adds a new USB gadget function exposing CDC-NCM so the target host sees a
USB Ethernet device. Off by default; toggled via setUsbDeviceState("ncm",
true) or the new "Enable Ethernet over USB (CDC-NCM)" checkbox under the
custom USB devices preset.

host_addr and dev_addr are derived deterministically from GetDeviceID()
via SHA-256, with the locally-administered bit set, so MACs stay stable
across reboots and never collide across devices on the same host.

Reachability uses IPv6 link-local only: as soon as the link comes up, the
kernel auto-assigns fe80::/10 from the dev_addr MAC via modified EUI-64.
This works zero-config on Windows, macOS, and Linux (the host's NM/NetCfg
brings the new netdev up; on minimal Linux setups `ip link set <iface> up`
is needed once). IPv4 link-local (APIPA) is intentionally not configured
here -- it requires an extra address on our side and host-side fallback
support that varies by distro; defer to a follow-up if needed.

Bring-up/teardown of the usb0 netdev runs as a post-transaction hook in
configureUsbGadget and uses vishvananda/netlink (already a dependency
via pkg/nmlite/link) rather than shelling out to `ip`.

Scope is deliberately minimal: no NAT, no DHCP server, no internet
sharing, no bridge mode, no other Ethernet protocols. This is the base
layer that future work (clipboard sync over IP, file transfer, etc.) can
build on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an `inet jetkvm` table with a single `input_usb0` chain that drops
TCP and UDP arriving on usb0. ICMP/ICMPv6 (including NDP and ping) are
untouched so the link stays debuggable. Installed by applyNcmFirewall
when NCM is toggled on, removed by removeNcmFirewall on toggle off.

Fail-closed: if modprobe or any nftables operation fails, the link is
rolled back and bringUpNcmInterface returns the error -- no scenario
where usb0 is up without the firewall in place. Idempotent: a stale
table from a previous run is deleted before the fresh one is built.

The rv1106 rootfs ships nf_tables.ko in /lib/modules but does not
auto-load it (kernel module auto-load via NETLINK_NETFILTER never
fires here), so applyNcmFirewall does a lazy `modprobe nf_tables` on
first use. Match expressions (meta iifname, meta l4proto, cmp, drop)
are all baked into nf_tables.ko on 5.10, so no additional modprobes
are needed for the production rules.

This closes the security gap that was previously documented as a
known issue in PR jetkvm#1470: with this in place, the web UI, mDNS, SSH
and any other system services are unreachable from the target host
over the CDC-NCM link, while ICMPv6 (NDP + ping) keeps the link
itself debuggable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The RV1106 dwc3 controller has a limited number of IN (device->host) and OUT
(host->device) endpoints -- separate pools, ~7 each. Each gadget function
claims some: HID mice/wake and audio capture take 1 IN; the keyboard takes
1 IN + 1 OUT (LED reports); mass storage takes 1 IN + 1 OUT; CDC-ACM and
CDC-NCM take 2 IN + 1 OUT each. When the enabled set exceeds either pool a
function silently fails to allocate its endpoint(s) -- CDC-NCM in particular
comes up looking connected (RX works) while TX is a black hole. That's
painful to diagnose, so warn before the user commits to an over-budget combo.

- internal/usbgadget/endpoints.go: per-function {in,out} endpoint cost map,
  the two budget constants (with the empirical derivation documented; the IN
  ceiling of 7 is confirmed on-device, OUT is assumed symmetric), and
  ExceedsEndpointBudget. The true low-level limiter may be dwc3 TX-FIFO SRAM
  rather than a raw count, but the effective ceiling is the same; FIFO RAM is
  not modeled separately.
- internal/usbgadget/config.go: extract isGadgetConfigItemEnabledForDevices so
  endpoint demand can be computed for any hypothetical Devices selection.
- jsonrpc.go: getUsbEndpointReport RPC returning {exceedsBudget} for a given
  device set.
- UsbDeviceSetting.tsx: query it live as toggles change and show an inline
  amber warning when the set exceeds the budget, suggesting the user free an
  endpoint (e.g. disable Relative Mouse).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mcuelenaere mcuelenaere force-pushed the claude/optimistic-torvalds-0a8c56 branch from 532f30c to a62baa7 Compare May 29, 2026 13:38
@mcuelenaere mcuelenaere marked this pull request as ready for review May 29, 2026 13:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant